Add Tenuo authorization contrib module#1447
Add Tenuo authorization contrib module#1447aimable100 wants to merge 2 commits intotemporalio:mainfrom
Conversation
Adds `temporalio.contrib.tenuo`, a SimplePlugin integration for
Tenuo warrant-based authorization in Temporal workflows.
The plugin (`TenuoPlugin`) wires client interceptors, worker
interceptors, and workflow sandbox passthrough in a single line:
from temporalio.contrib.tenuo import TenuoPlugin
plugin = TenuoPlugin(config)
client = await Client.connect("localhost:7233", plugins=[plugin])
Key design decisions:
- Thin adapter: only TenuoPlugin, TENUO_PLUGIN_NAME, and
ensure_tenuo_workflow_runner are exported from the contrib module.
All other types (TenuoPluginConfig, EnvKeyResolver, etc.) are
imported directly from tenuo.temporal.
- No private imports: all tenuo.temporal internals used by the plugin
are exposed through public lazy-loaded names.
- No re-exports of external package types, matching the pattern
established by openai_agents and other contrib modules.
Files:
- temporalio/contrib/tenuo/__init__.py — public API (3 exports)
- temporalio/contrib/tenuo/_plugin.py — TenuoPlugin SimplePlugin subclass
- temporalio/contrib/tenuo/README.md — multi-agent delegation example
- tests/contrib/tenuo/test_tenuo.py — unit + live integration tests
- tests/contrib/tenuo/test_tenuo_replay.py — record-and-replay tests
- pyproject.toml — tenuo optional dependency
Add Tenuo authorization contrib module
|
I don't think it is likely that we are willing to accept this. We welcome folks using Temporal as a part of their solution, but including it in the SDK's contrib comes with an implication of our maintenance and ownership of the solution. From a technical perspective, you are welcome to create a plugin external to the SDK repo, and we can have a discussion about partnership. If you reach out in our community slack, I can put you in touch with the folks running AI partnership. |
|
Thanks for the note. This was submitted through Temporal's AI Partner Program — I was invited and completed the submission form. Happy to move to an external plugin if that's the preferred path for partners too. Will follow up with the team to confirm. |
|
@aimable100 - thank you for preparing this plugin. I will leave this PR open so that our team can provide feedback. You should plan to move it to one of your repositories, though. |
This comment was marked as low quality.
This comment was marked as low quality.
DABH
left a comment
There was a problem hiding this comment.
This plugin appears to be following the recommended strategy of using SimplePlugin's interface. The plugin is also performing authorization by intercepting activities - interceptors are indeed designed to perform actions like this as seen in the OpenTelemetryPlugin and BraintrustPlugin, so no concerns there. Replay testing is being taken seriously here, which is nice to see.
One overall concern is the naming/shipping strategy here. Tenuo appears to already ship a Temporal plugin (https://tenuo.ai/temporal). The present plugin imports a bunch of stuff (including TenuoPlugin) from tenuo.temporal, so it seems like the present PR is really a thin wrapper/adapter around what's already been published. Can you explain how the present PR is different than the existing plugin, and whether both need to exist? On naming, Tenuo's README on tenuo-ai/tenuo uses the name TenuoTemporalPlugin, while the integration docs page uses TenuoPlugin, and the PR adds a third binding temporalio.contrib.tenuo.TenuoPlugin - three names in three places could be a little confusing.
I left a few inline comments and suggestions in the code below. Happy to continue the conversation and keep iterating. Thank you for your efforts!
## Summary Addresses worker-side feedback from the Temporal team (DABH) on [temporalio/sdk-python#1447](temporalio/sdk-python#1447). **Plugin (`tenuo-python/tenuo/temporal_plugin.py`)** - No longer mutates the user's `TenuoPluginConfig`; works on a shallow copy so two workers sharing a config stay isolated. - Registers Tenuo's domain exceptions (`TenuoContextError`, `PopVerificationError`, `TemporalConstraintViolation`, `WarrantExpired`, `ChainValidationError`, `KeyResolutionError`, `LocalActivityError`) as `workflow_failure_exception_types` on SDKs that support it. - Preload failures log at `ERROR` with the resolver class name; `EnvKeyResolver` preload failure raises `ConfigurationError` (no safe `os.environ` fallback in the sandbox). - `ensure_tenuo_workflow_runner` emits a `UserWarning` plus a logger warning when given `UnsandboxedWorkflowRunner` (Tenuo still works — the user is just opting out of Temporal's own determinism guardrails, which is a legitimate choice for debugging), and warns for unknown custom runners. - Duplicate-registration error now points at `Client.connect(plugins=[plugin])` inheritance instead of advising one-plugin-per-worker. **Plugin-confusion rename (`tenuo.temporal.TenuoPlugin` → `TenuoWorkerInterceptor`)** - The old name was a Temporal SDK `WorkerInterceptor`, not a Temporal SDK `Plugin`, and its resemblance to `tenuo.temporal_plugin.TenuoTemporalPlugin` caused real misconfigurations (e.g. `Worker(plugins=[TenuoPlugin(...)])` silently accepting an unusable argument). - New canonical name: `tenuo.temporal.TenuoWorkerInterceptor`. - Backward compat: `tenuo.temporal.TenuoPlugin` is still importable as a deprecated alias and emits a `DeprecationWarning` on first resolution; scheduled for removal in a future beta. Most users register `TenuoTemporalPlugin` via `Client.connect(plugins=[plugin])` and are unaffected. - Updated all internal usages, tests, examples (5 files), and docs. Added an "About the names" callout table in `docs/temporal.md` and a "renamed from" breadcrumb in `docs/temporal-reference.md`. - New unit test asserts the alias warns and resolves to the new class. **Tests** - `DictKeyResolver` raises `KeyResolutionError` instead of `ValueError`. - 7 new unit tests in `tests/adapters/test_temporal_plugin.py` cover every plugin-side change above, plus the deprecation-alias test. **Deferred to follow-ups** - Making `ensure_tenuo_workflow_runner` private — useful public escape hatch for advanced users; keep public. - Replay-time negative tests (tampered history, rotated trusted roots, clock-boundary). Initial attempts revealed that the current plugin architecture does not re-verify activity PoP during replay — activities don't re-execute, and the workflow inbound interceptor only stashes headers without re-checking. Designing meaningful replay-safety tests requires plumbing changes and should be scoped as its own task. ## Test plan - [x] `uv run pytest tests/adapters/test_temporal_plugin.py` — 33 passed (incl. new deprecation test). - [x] `uv run pytest tests/adapters/test_temporal.py tests/adapters/test_transparent_interceptor.py tests/adapters/test_temporal_integration.py tests/e2e/test_temporal_replay.py` — 166 passed. - [x] `uv run pytest tests/e2e/test_temporal_e2e.py tests/e2e/test_temporal_replay.py` — 61 passed. - [x] `uv run pytest tests/security/test_security_contracts.py tests/security/test_integration_invariants.py` — 117 passed, 22 skipped. - [x] `uvx ruff check` clean on modified files. - [x] `mypy tenuo/temporal/__init__.py tenuo/temporal/_interceptors.py tenuo/temporal_plugin.py` — no errors. - [x] All 5 Temporal examples byte-compile.
|
Thanks @DABH. Per-comment replies inline. Plan is to withdraw this PR, per @jssmith's guidance. Shipping One bonus finding worth flagging: your tampered-history References:
Thanks again for the thorough pass. |
Summary
Adds
temporalio.contrib.tenuo, aSimplePluginthat wires Tenuo warrant-based authorization into Temporal workflows. Agents (workflows) carry signed warrants specifying which tools (activities) they can call and with what argument constraints. Sub-agents (child workflows) receive attenuated warrants — capabilities can only shrink, never expand.TenuoPlugin— registers client interceptor (warrant header injection), worker interceptors (PoP signing + authorization verification), and sandbox passthrough for thetenuonative extension.TenuoPlugin,TENUO_PLUGIN_NAME,ensure_tenuo_workflow_runner). All other types are imported fromtenuo.temporal, matching the pattern established byopenai_agentsand other contrib modules.tenuo.temporalinternals used by the plugin are exposed through public lazy-loaded names.Files
temporalio/contrib/tenuo/__init__.pytemporalio/contrib/tenuo/_plugin.pytemporalio/contrib/tenuo/README.mdtests/contrib/tenuo/test_tenuo.pytests/contrib/tenuo/test_tenuo_replay.pypyproject.tomlReplay safety
Replay determinism is verified at two levels:
workflow.now()(nottime.time()), nodatetime.now(), noos.urandom/random/uuid4, notime.sleep, nothreading.Thread.fetch_history(), and a freshTenuoPlugininstance replays viaReplayer. Tests cover single-tool and multi-tool (sequential PoP ordering) scenarios.Integration tests
test_authorized_activity_succeeds— full warrant → PoP → authorization flowtest_start_workflow_authorized—start_workflow_authorizedreturns a handletest_unauthorized_activity_is_non_retryable— unauthorized tool call producesWorkflowFailureErrorwithApplicationError(non_retryable=True)test_duplicate_registration_raises— same plugin instance on two workers raisesRuntimeErrorTest plan
pytest tests/contrib/tenuo/test_tenuo.py -v— unit + integration testspytest tests/contrib/tenuo/test_tenuo_replay.py -v— replay determinism testsruff check temporalio/contrib/tenuo/ tests/contrib/tenuo/— no lint errors